package compile;

import common.FileUtil;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

// 每次“编译运行”的过程就是一个Task
public class Task {
    // 通过一组常量来约定临时文件的名字
    // 以下就是后续要读写的文件，这些都是文本文件，所以应该使用字节流更合适一些

    // 表示所有临时文件的目录
    // 进行修改：使用UUID针对每一个请求生成唯一目录
    private String WORK_DIR = null;
    // 约定代码的类名
    private String CLASS = null;
    // 约定要编译的代码文件名
    private String CODE = null;
    // 约定存放编译错误信息的文件名
    private String COMPILE_ERROR = null;
    // 约定存放运行时的标准输入的文件名
    private String STDOUT = null;
    // 约定存放运行时的标准错误的文件名
    private String STDERR = null;

    // 此时不再定义为static final形式，而是使用构造方法进行初始化
    public Task() {
        // 使用UUID这个类就能生成一个UUID
        WORK_DIR = "./tmp/" + UUID.randomUUID().toString() + "/";
        CLASS = "Solution";
        CODE = WORK_DIR + CLASS + ".java";
        COMPILE_ERROR = WORK_DIR + "compileError.txt";
        STDOUT = WORK_DIR + "stdout.txt";
        STDERR = WORK_DIR + "stderr.txt";
    }

    // 该Task类的核心方法是compileAndRun，即“编译和运行”
    // 参数：要运行编译的java源代码
    // 返回值：表示编译运行的结果：编译错误、运行错误、运行正确...  【创建专门的类来进行表示】
    public Answer compileAndRun(Question question) {
        Answer answer = new Answer();

        // 0. 先准备好用来存放临时文件的目录
        File workDir = new File(WORK_DIR);
        if(!workDir.exists()) {
            // 创建多级目录
            workDir.mkdirs();
        }

        // 0. 进行安全性判断
        if (!checkCodeSecurity(question.getCode())) {
            System.out.println("用户提交了不安全的代码！");
            answer.setError(3);
            answer.setReason("您提交的代码可能会危害到服务器，禁止运行！");
            return answer;
        }

        // 1. 将question里的code写入到一个Solution.java文件中
        FileUtil.writeFile(CODE,question.getCode());

        // 2. 需要创建子进程调用javac进行编译，但是注意：在进行编译的时候需要的是一个.java文件，当前是通过String的方式来进行传参的
        //    如果编译出错，javac就会把编译错误信息写入到stderr中，可以使用一个专门的文件来进行保存：compileError.txt
        //    需要先把编译命令给构造出来:-d用来指定保存的目录
        String compileCmd = String.format("javac -encoding utf8 %s -d %s",CODE,WORK_DIR);
        System.out.println("编译命令：" + compileCmd);
        CommandUtil.run(compileCmd,null,COMPILE_ERROR); // 注意编译不关注标准输出
        // 判断编译是否通过
        // 如果编译出错，错误信息就被记录到COMPILE_ERROR这个文件中；若没有编译出错，该文件就是空文件
        String compileError = FileUtil.readFile(COMPILE_ERROR);
        if(!compileError.equals("")) {
            // 编译失败
            // 直接返回Answer，Answer中记录编译的错误信息
            System.out.println("编译出错！");
            answer.setError(1);
            answer.setReason(compileError);
            return answer;
        }
        // 编译成功，继续执行运行逻辑

        // 3. 创建子进程，调用java命令并执行
        //    运行程序的时候，也会把java子进程中的标准输出和标准错误获取到：stdout.txt，stderr.txt
        String runCmd = String.format("java -classpath %s %s",WORK_DIR,CLASS);
        System.out.println("运行命令：" + runCmd);
        CommandUtil.run(runCmd,STDOUT,STDERR);
        // 正常情况下，运行结果的标准错误中是不应该存在内容的，即为空
        String runError = FileUtil.readFile(STDERR);
        if(!runError.equals("")) {
            // 运行失败
            System.out.println("运行出错！");
            answer.setError(2);
            answer.setReason(runError);
            return answer;
        }
        // 运行成功

        // 4. 父进程获取到刚才编译执行的结果，并打包成Answer对象进行返回
        //    编译执行的结果就通过刚才的文件进行获取即可
        answer.setError(0);
        answer.setStdout(FileUtil.readFile(STDOUT));
        return answer;
    }

    private boolean checkCodeSecurity(String code) {
        // 设定一个黑名单
        List<String> blackList = new ArrayList<>();
        // 防止提交的代码运行恶意程序
        blackList.add("Runtime");
        blackList.add("exec");
        // 禁止提交的代码读写文件
        blackList.add("java.io");
        // 禁止提交的代码访问网络
        blackList.add("java.net");

        // 进行校验
        for (String str: blackList) {
            int pos = code.indexOf(str);
            if(pos >= 0) {
                // 找到了恶意代码特征，就不安全，返回false
                return false;
            }
        }
        // 遍历结束后还没有发现恶意代码特征，安全
        return true;
    }

    // 测试
    public static void main(String[] args) {
        Task task = new Task();
        Question question = new Question();
        question.setCode("public class Solution {\n" +
                "    public static void main(String[] args) {\n" +
                "        System.out.println(\"hello Xianmeng!\");\n" +
                "    }\n" +
                "}\n");
        Answer answer = task.compileAndRun(question);
        System.out.println(answer);
    }
}
